Structured lookup
For tabular documents (Excel .xlsx or .csv), semantic search is often the wrong tool. When a file holds structured records — people with a name, phone and address; products with a SKU; accounts with an id — you usually want to look a record up by a specific column, not retrieve semantically-similar text.
A structured lookup exposes a dedicated tool that looks records up in such a document: the agent calls it with a value (for example, a caller's name) and gets back the matching row (or a short list of candidates). Matching is fuzzy and phonetic, so speech-to-text errors like Kaitlyn Meyers still resolve to Caitlin Myers. A lookup can also match on more than one column at once — by phone and city, say — when one column alone doesn't identify a record.
Configuring any lookup makes the document lookup-only: its files are not embedded and are not available through RAG, doc_search or doc_get. Attach it to an agent like any other document — the agent gains the lookup tool(s) automatically, in both text and speech-to-speech modes. A single agent can freely mix lookup documents and ordinary RAG documents.
A Flow can use a lookup document the same way, by attaching it to a conversation node, so the node grounds its responses on the retrieved record. Alternatively, the #lookup directive turns such a node into the equivalent of a Call tool node for the lookup: the result never reaches the model, and the flow branches on the outcome.
Configuring a lookup
Lookups are defined in the document's Advanced configuration, under a lookups array. Each entry becomes one tool:
{
"lookups": [
{
"tool_name": "lookup_customer",
"tool_description": "Look up a customer's account by name.",
"search_column": "Full Name",
"return_columns": ["Full Name", "Account ID", "City"]
}
]
}
Only tool_name and search_column are required. Everything else is optional:
| Field | Description | Default |
|---|---|---|
tool_name
|
Name of the tool exposed to the agent. Required. | — |
search_column
|
Primary column matched against the caller value — a column letter (A, B, …) or, when the file has a header row, a header name. Required. |
— |
additional_search_columns
|
Up to 2 further columns to match on as well — see Matching on additional columns. | none |
file
|
Which file to search, when the document has more than one. | first Excel/CSV file |
sheet
|
Worksheet name (Excel only). | first sheet |
header_row
|
1-based row of the header; rows above it are skipped. Use 0 for a file with no header (columns are then referenced only by letter). |
1 (auto) |
tool_description
|
Description shown to the agent. | auto-generated |
param_name
|
Name of the tool argument carrying the search_column value (for example phone). |
the column name as an identifier (Full Name → full_name) |
return_columns
|
Columns to include in the result (letters or names); unknown columns are ignored, and every matched column is always included. | all columns |
match_mode
|
exact, fuzzy (typo-tolerant), phonetic (sounds-alike), or hybrid (both). |
hybrid
|
min_score
|
Minimum match score (0–100) for a row to be a candidate. | 75
|
confident_score
|
A single match at or above this score, clearly ahead of the rest, is returned as a confident match rather than as candidates. | 92
|
max_candidates
|
Maximum candidates returned when the match is ambiguous. | 5
|
debug
|
Include a scores list in the result to help tune the thresholds (see below). Disable in production. |
false
|
Note: A param_name that collides with a reserved field (such as name) is renamed internally and falls back to query where needed. Prefer distinctive names like customer_name or phone.
Referencing columns
Columns may be referenced two ways:
- By letter —
A,B,C, … (spreadsheet style). Always available, and the only option when the file has no header. - By header name — available when the file has a header row (the default). Matching is case-insensitive.
If a value matches both a header name and a letter, the header name wins.
Matching on additional columns
One column often isn't enough to identify a record — two customers share a name, or a phone number appears against several accounts. Add up to two more columns under additional_search_columns, and each becomes its own tool argument:
{
"lookups": [
{
"tool_name": "find_customer",
"search_column": "Phone",
"param_name": "phone",
"match_mode": "exact",
"additional_search_columns": [
{ "column": "City", "param_name": "city", "match_mode": "hybrid" }
],
"return_columns": ["Full Name", "Account ID", "City"]
}
]
}
Each entry takes:
| Field | Description | Default |
|---|---|---|
column
|
The column to match — a letter or a header name, exactly like search_column. Required. |
— |
param_name
|
Name of the tool argument carrying this column's value. | the column name as an identifier (Home City → home_city) — or query_<column> when that would be too short to mean anything (B → query_b) |
param_description
|
Description of the tool argument. | auto-generated |
match_mode
|
Matching strategy for this column. | the lookup's match_mode |
A record must match every value the agent supplies. That is what makes an extra column narrow the search rather than widen it: the phone above is matched exactly while the city stays fuzzy, so a caller whose city was mis-heard is still found, but a record with the right phone and the wrong city is not returned at all.
Two consequences worth knowing:
search_columnis required, additional columns are optional. The agent must always supply the primary value; it supplies the others only when it knows them. Leaving one out is not an error — the lookup simply runs on the columns it was given, which is more likely to come back as ambiguous. That is the intended conversation: look up the phone, get three candidates, ask the caller which city, then look up again with both.- A row scores as its weakest matched column. So
min_scoreandconfident_scorekeep the meaning they have for a single-column lookup, and a column that every candidate matches equally well changes nothing. Setmatch_modeper column rather than trying to give one column a looser threshold.
A row is indexed as long as it fills at least one search column. A blank cell in an additional column never matches a supplied value, so such a row is unreachable through that column while staying findable by the ones it does fill.
What the agent receives
The tool returns one of three outcomes:
- A confident single match with the row's data.
- An ambiguous result — a short list of candidate rows. The tool description instructs the agent to ask the caller to disambiguate (for example, "I found Sean Riley and Shawn Reilly — which one did you mean?").
- Not found — nothing scored above
min_score.
The tool description always tells the model to pass the value exactly as heard and not to "correct" it, because the fuzzy/phonetic layer handles STT errors better than the model's own guesses.
Tuning the thresholds
Good values for min_score and confident_score depend on your data, and the scores aren't obvious up front. Set debug to true while testing: every result then carries a scores list — the top matching rows with their raw scores, ignoring the thresholds — so you can see exactly how real queries score. Each entry breaks the row's score down per column, so you can tell which column rejected it.
For example, a not_found result then looks like:
{
"result": "not_found",
"scores": [
{"score": 72, "keys": [{"column": "Full Name", "value": "Caitlin Myers", "score": 72}]},
{"score": 58, "keys": [{"column": "Full Name", "value": "Colin Meyer", "score": 58}]}
]
}
Here the intended record scored 72 but the default min_score of 75 rejected it — so lowering min_score to around 70 would let it through. Once tuned, set debug back to false (or remove it) so the scores aren't returned in production.
Table size
A single lookup indexes at most 10,000 rows. Every match mode except exact compares the caller's value against every indexed row, so this limit is what keeps a lookup fast enough to run inside a conversation turn. Adding more search columns doesn't change it.
Multiple files and lookups
A document may define several lookups — for example a customer file searchable by name and a separate lookup by phone, or two different files. Each lookup targets one file (via file) and produces its own tool. Files in the document that no lookup references are unused; this is reported on the document's status.
Viewing the indexed data
Because a lookup document is not chunked for RAG, the Info dialog shows the indexed rows instead: the row count per file and a preview of the rows (capped for large files). Any configuration problem — a missing search column, a file with no matching column — is reported in the document's status.